package repository;

import gitobject.Blob;
import gitobject.Commit;
import gitobject.GitObject;
import gitobject.Tree;
import utility.Utility;

import java.io.*;
import java.nio.file.Path;
import java.util.*;

public class Index {
    public static class Entry{
        public Entry(){}
        /**
         *
         * @param key blob的哈希值
         * @param last_access_time 文件最后访问日期
         */
        public Entry(byte[] key, long last_access_time) {
            this.key = key;
            this.last_access_time = last_access_time;
        }

        public byte[] getKey() {
            return key;
        }

        public long getLast_access_time() {
            return last_access_time;
        }

        byte[] key;
        long last_access_time;
    }

    Map<Path, Entry> stage = new HashMap<>();
    public Map<Path, Entry> getStage() {
        return stage;
    }

    protected Index(){}
    /**
     * blob对象的add方法
     * @param blob blob对象
     */
    public void add(Blob blob){
        Path path = blob.getPath();
        byte[] key = blob.getKey();
        long time = blob.getLastModified();
        stage.put(path, new Entry(key, time));
    }

    /**
     * 从暂存区中删除方法
     * @param path 文件路径
     * @throws IOException
     */
    public void remove(Path path) throws IOException {
        if(!stage.containsKey(path)){
            System.err.printf("file %s is not in stage.", path);
            System.exit(-1);
        }
        stage.remove(path);
    }
    /**
     * 生成tree对象
     * @return 返回值为生成的tree对象
     * @throws IOException
     */
    public Tree buildTree() throws IOException {
        var root_tree = new Tree();
        Map<Path, Tree> treeMap = new HashMap<>();
        root_tree.setPath(Path.of(""));
        for (var record: stage.entrySet()){
            Path path = record.getKey();
            Entry entry = record.getValue();
            GitObject blob = GitObject.findByKey(entry.key);
            assert blob instanceof Blob;
            Stack<Path> pathStack = new Stack<>();
            Path parent = path.getParent();
            while (parent != null) {
                pathStack.push(parent);
                parent = parent.getParent();
            }
            Tree parent_tree = root_tree;
            while (!pathStack.empty()){
                Path cur_path = pathStack.pop();
                Tree tree = treeMap.get(cur_path);
                if(tree == null){
                    tree = new Tree();
                    parent_tree.add(tree);
                    treeMap.put(cur_path, tree);
                    tree.setPath(cur_path);
                }
                parent_tree = tree;
            }
            parent_tree.add(blob);
        }
        return root_tree;
    }

    public List<Path> switchTo(byte[] commit_key) {
        return switchTo(Utility.hex2string(commit_key));
    }

    /**
     * 切换暂存区到指定的commit记录
     * @param commit_key 参数为哈希值
     * @return
     */
    public List<Path> switchTo(String commit_key){
        try {
            GitObject object = GitObject.findByKey(commit_key);
            if(object.getType() != GitObject.Type.Commit){
                throw new IllegalArgumentException(commit_key + " Is Not A Commit.");
            }
            Commit commit = (Commit) object;
            Set<Path> needRemove = new HashSet<>(stage.keySet());
            stage.clear();
            object = GitObject.findByKey(commit.getTree());
            if(object.getType() != GitObject.Type.Tree){
                throw new IllegalArgumentException(commit.getTree()+ " Is Not A Tree.");
            }
            Tree tree = (Tree) object;
            Queue<Tree> treeQueue = new ArrayDeque<>();
            treeQueue.add(tree);
            while (!treeQueue.isEmpty()){
                Tree cur_tree = treeQueue.poll();
                for(GitObject item: cur_tree.getObjectTree()){
                    switch (item.getType()){
                        case Blob:
                            add((Blob) item);
                            needRemove.remove(((Blob) item).getPath());
                            break;
                        case Tree:
                            treeQueue.add((Tree) item);
                            break;
                        default: throw new RuntimeException("Bad Object File: " + Utility.hex2string(item.getKey()));
                    }
                }
            }
            return needRemove.stream().toList();
        } catch (IOException e){
            throw new RuntimeException(e);
        }
    }

    public void write() throws IOException{
        write(Repository.getInstance().getIndexPath().toFile());
    }

    /**
     * 写入功能函数
     * @param file 参数为文件对象
     * @throws IOException
     */
    public void write(File file) throws IOException {
        var out = new DataOutputStream(
                new BufferedOutputStream(new FileOutputStream(file))
        );
        out.writeInt(stage.size());
        for(var item: stage.entrySet()){
            Entry entry = item.getValue();
            out.writeUTF(item.getKey().toString());
            out.write(entry.key, 0, 20);
            out.writeLong(entry.last_access_time);
        }
        out.close();
    }

    public static Index fromFile() throws IOException {
        return fromFile(Repository.getInstance().getIndexPath().toFile());
    }
    /**
     * 从文件加载index数据
     * @param file 参数为文件对象
     * @return 返回值为index
     * @throws IOException
     */
    public static Index fromFile(File file) throws IOException {
        if(file.exists() && !file.isFile()){
            throw new RuntimeException(new IllegalArgumentException(String.format("%s is not a file.", file)));
        }
        if(!file.exists()){
            return null;
        }
        var index = new Index();
        var in = new DataInputStream(new BufferedInputStream(new FileInputStream(file)));
        int size = in.readInt();
        for(int i=0; i< size; i++){
            Path path = Path.of(in.readUTF());
            byte[] key = in.readNBytes(20);
            long last_access_time = in.readLong();
            index.stage.put(path, new Entry(key, last_access_time));
        }
        return index;
    }
}
